package com.fable.sp.core.template.freemarker;

import com.fable.sp.core.config.SharedVariableProducer;
import com.fable.sp.core.template.TemplateNotFoundException;
import com.fable.sp.core.template.TemplateRenderException;
import com.fable.sp.core.template.TemplateRenderer;
import com.fable.sp.core.util.Codecs;
import com.fable.sp.core.util.HttpUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.StringTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.ext.servlet.*;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateModelException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.view.AbstractTemplateView;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;

/**
 * .
 *
 * @author stormning on 16/8/5.
 */
public class FreemarkerTemplateRender implements TemplateRenderer, ServletContextAware, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(FreemarkerTemplateRender.class);

    private org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer configurer;

    private StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();

    private Configuration configuration;

    @Autowired(required = false)
    private List<SharedVariableProducer> sharedVariableProducers = Lists.newArrayList();

    private ServletContext servletContext;
    private ServletContextHashModel servletContextHashModel;
    private TaglibFactory taglibFactory;

    public void setConfigurer(org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer configurer) {
        this.configurer = configurer;
    }

    @Override
    public String renderNamedTemplate(String name, Map<String, Object> params) throws TemplateRenderException, TemplateNotFoundException {
        return render(getTemplate(name), params);
    }

    @Override
    public void renderNamedTemplate(String name, Map<String, Object> params, Writer writer) throws TemplateRenderException, TemplateNotFoundException {
        render(getTemplate(name), params, writer);
    }

    private Object merge(Map<String, Object> params) {
        Map<String, Object> sharedVariables = getSharedVariables();
        if (params == null) {
            return sharedVariables;
        }
        params.putAll(sharedVariables);

        HttpServletRequest request = HttpUtil.getCurrentRequest();
        HttpServletResponse response = HttpUtil.getCurrentResponse();
        WidgetHttpScopesHashModel fmModel = new WidgetHttpScopesHashModel(configuration.getObjectWrapper(), servletContext, request, params);
        fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory);
        fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel);
        fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response));
        fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper()));
        fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request));
        // Expose RequestContext instance for Spring macros.
        try {
            fmModel.put(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
                    new RequestContext(request, response, servletContext, fmModel.toMap()));
        } catch (TemplateModelException e) {
            e.printStackTrace();
        }
        fmModel.putAll(params);
        return fmModel;
    }

    private Map<String, Object> getSharedVariables() {
        Map<String, Object> sharedVariables = Maps.newHashMap();
        for (SharedVariableProducer producer : sharedVariableProducers) {
            Map<String, Object> variables = producer.produceVariables();
            if (!CollectionUtils.isEmpty(variables)) {
                sharedVariables.putAll(variables);
            }
        }
        return sharedVariables;
    }

    @Override
    public String renderStringTemplate(String fragment, Map<String, Object> params) throws TemplateRenderException, TemplateNotFoundException {
        return render(getStringTemplate(fragment), params);
    }

    @Override
    public void renderStringTemplate(String name, Map<String, Object> params, Writer writer) throws TemplateRenderException, TemplateNotFoundException {
        render(getTemplate(name), params, writer);
    }

    public void render(Template template, Map<String, Object> params, Writer writer) throws TemplateRenderException {
        try {
            template.process(merge(params), writer);
        } catch (Exception e) {
            LOGGER.error("Render template error.", e);
            throw new TemplateRenderException(e);
        }
    }

    public String render(Template template, Map<String, Object> params) throws TemplateRenderException {
        StringWriter stringWriter = new StringWriter();
        render(template, params, stringWriter);
        return stringWriter.toString();
    }


    private Template getTemplate(String name) throws TemplateNotFoundException {
        try {
            return configuration.getTemplate(name);
        } catch (IOException e) {
            throw new TemplateNotFoundException(e);
        }
    }

    private Template getStringTemplate(String content) throws TemplateNotFoundException {
        String key = Codecs.hash(content);
        if (stringTemplateLoader.findTemplateSource(key) != null) {
            stringTemplateLoader.putTemplate(key, content);
        }
        return getTemplate(key);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.configuration = configurer.getConfiguration();
        initServletContext();
        TemplateLoader tl = configuration.getTemplateLoader();
        if (tl == null) {
            configuration.setTemplateLoader(stringTemplateLoader);
        } else {
            configuration.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[]{tl, stringTemplateLoader}));
        }
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }


    protected void initServletContext() throws BeansException {
        this.taglibFactory = new TaglibFactory(servletContext);

        GenericServlet servlet = new GenericServletAdapter();
        try {
            servlet.init(new DelegatingServletConfig());
        } catch (ServletException ex) {
            throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex);
        }
        this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper());
    }

    public ObjectWrapper getObjectWrapper() {
        return configuration.getObjectWrapper();
    }

    private HttpSessionHashModel buildSessionModel(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            return new HttpSessionHashModel(session, getObjectWrapper());
        } else {
            return new HttpSessionHashModel(null, request, response, getObjectWrapper());
        }
    }

    private static class GenericServletAdapter extends GenericServlet {
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
            // no-op
        }
    }

    /**
     * Internal implementation of the {@link ServletConfig} interface,
     * to be passed to the servlet adapter.
     */
    private class DelegatingServletConfig implements ServletConfig {
        @Override
        public String getServletName() {
            return getClass().getName();
        }

        @Override
        public ServletContext getServletContext() {
            return servletContext;
        }

        @Override
        public String getInitParameter(String paramName) {
            return null;
        }

        @Override
        public Enumeration<String> getInitParameterNames() {
            return Collections.enumeration(new HashSet<String>());
        }
    }
}
